The first step is to use defensive programming, i.e., to assume that mistakes will happen and to guard against them. One way to do this is to add assertions to our code so that it checks itself as it runs. An assertion is simply a statement that something must be true at a certain point in a program. When Python sees one, it checks that the assertion's condition. If it's true, Python does nothing, but if it's false, Python halts the program immediately and prints the error message provided.
For example, suppose we are representing rectangles using a tuple of four coordinates $(x_0, y_0, x_1, y_1)$. In order to do some calculations, we need to normalize the rectangle so that it is at the origin and 1.0 units long on its longest axis. This function does that, but checks that its input is correctly formatted and that its result makes sense:
In [164]:
def normalize_rectangle(rect):
"""
Normalize a rectangle
Parameter
---------
rect tuple or list of floats
coordinates of the rectangle (x0, y0, x1, y1)
Returns
-------
norm_rect tuple or list of floats
coordinates of the rectangle (0., 0., x1_n, y1_n)
"""
assert len(rect) == 4, 'Rectangles must contain 4 coordinates'
x0, y0, x1, y1 = rect
assert x0 < x1, 'Invalid X coordinates'
assert y0 < y1, 'Invalid Y coordinates'
dx = x1 - x0
dy = y1 - y0
if dx > dy:
scaled = float(dx) / dy
upper_x, upper_y = 1.0, scaled
else:
scaled = float(dx) / dy
upper_x, upper_y = scaled, 1.0
assert 0 < upper_x <= 1.0, 'Calculated upper X coordinate invalid'
assert 0 < upper_y <= 1.0, 'Calculated upper Y coordinate invalid'
return (0, 0, upper_x, upper_y)
The preconditions on lines 2, 4, and 5 catch invalid inputs:
In [163]:
print normalize_rectangle( (0.0, 1.0, 2.0) ) # missing the fourth coordinate
In [161]:
print normalize_rectangle( (0.0, 0.0, 5.0, 1.0) )
In [153]:
#first making a test function
def test_adder(fad):
"""
Function that tests a adder function
Parameter
---------
fad func(float,float)
function that takes two floats as input
Returns
-------
None
Raises
------
AssertionError The function passed as parameter failed the test
"""
assert fad(1,1) == 2, '1+1=2'
assert fad(2,2) == 4, '2+2=4'
In [154]:
def adder(a,b):
return a * b
In [155]:
test_adder(adder)
Using the docstring as a --simplistic-- TDD.
In [113]:
def adder2(a,b):
"""
Function that adds two numbers
Parameters
----------
a float
b float
Returns
-------
c float
c = a + b
Example
-------
>>> adder2(2, 2)
4
>>> adder2(2, 3)
5
"""
return a * b
In [124]:
### Testing the all the docstrings loaded in memory
import doctest
doctest.testmod()
Out[124]:
Examples copied from :
In [18]:
%%file fibmodule.py
"""
Functions to compute Fibonacci sequences
"""
import numpy as np
from numpy.testing import assert_allclose
def fib(N):
"""
Compute the first N Fibonacci numbers
Parameters
----------
N : integer
The number of Fibonacci numbers to compute
Returns
-------
x : np.ndarray
the length-N array containing the first N
Fibonacci numbers.
Notes
-----
This is a pure Python implementation. For large N,
consider a Cython implementation
Examples
--------
>>> fib(5)
array([ 0., 1., 1., 2., 3.])
"""
x = np.zeros(N, dtype=float)
for i in range(N):
if i == 0:
x[i] = 0
elif i == 1:
x[i] = 1
else:
x[i] = x[i - 1] + x[i - 2]
return x
def test_first_ten():
nums = fib(10)
assert_allclose(fib(10),
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34])
Let's try to run the test to see if it fails
In [19]:
import fibmodule
fibmodule.test_first_ten()
nosetests
is a python suite to run tests automatically
The neat things is that it can also test the examples you put into your docstrings
In [69]:
%%writefile test_fibomodule2.py
import unittest
from fibmodule import fib
from numpy.testing import assert_allclose
class test_fibo(unittest.TestCase):
def test_first_ten2(self):
nums = fib(10)
assert_allclose(fib(10),
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34])
def test_negative(self):
"""Testing that `fib` raises a `ValueError` with a negative number as parameter"""
with self.assertRaises(ValueError):
fib(-1)
if __name__ == '__main__':
unittest.main()
In [70]:
%run test_fibomodule2.py
In [71]:
!nosetests -v fibmodule
In [72]:
!nosetests -v --with-doctest fibmodule
nosetests will run all the tests in all the files begining with "test_" in the dictionary
In [76]:
!nosetests -v
In [78]:
!nosetests -v --with-doctest *.py
In [74]:
In [74]:
In [ ]: